#![deny(warnings)]
extern crate pretty_env_logger;
#[macro_use]
extern crate log;

use hyper::header::HeaderValue;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode};
use serde::Deserialize;
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::sync::Arc;

#[derive(Debug, Deserialize)]
pub struct GeneralCfg {
    pub not_found_url: String,
}

#[derive(Debug, Deserialize)]
pub struct RedirectCfg {
    pub slug: String,
    pub to_url: String,
}

pub struct Config {
    pub general: GeneralCfg,
    pub redirects: HashMap<String, String>,
}

async fn handle_request(
    cfg: Arc<Config>,
    req: Request<Body>,
) -> Result<Response<Body>, hyper::Error> {
    if req.method() != &Method::GET {
        info!("Non-GET request received");
        return Ok(Response::builder()
            .status(StatusCode::METHOD_NOT_ALLOWED)
            .body(Body::empty())
            .unwrap());
    } else {
        let mut response = Response::new(Body::empty());
        let mut full_path = req.uri().path();
        let mut trailing = "";
        let mut matched = false;
        let mut no_match = false;
        let mut iter = req.uri().path().rmatch_indices('/');

        while matched == false && no_match == false {
            match cfg.redirects.get(full_path) {
                Some(redirect) => {
                    let mut dest = redirect.to_owned();
                    dest.push_str(trailing);
                    *response.status_mut() = StatusCode::FOUND;
                    if req.uri().query().is_some() {
                        dest.push_str("?");
                        dest.push_str(req.uri().query().unwrap());
                    }
                    response
                        .headers_mut()
                        .insert("Location", HeaderValue::from_str(&dest).unwrap());
                    matched = true;
                }
                _ => {
                    let next_el = iter.next();

                    match next_el {
                        Some(val) => {
                            let (first, second) = req.uri().path().split_at(val.0);
                            full_path = first;
                            trailing = second;
                        }
                        None => no_match = true,
                    }
                }
            }
        }

        if !matched {
            *response.status_mut() = StatusCode::FOUND;
            response.headers_mut().insert(
                "Location",
                HeaderValue::from_str(&cfg.general.not_found_url).unwrap(),
            );
        }
        return Ok(response);
    }
}

fn load_cfg() -> Result<Config, Box<dyn Error>> {
    let mut file = File::open("./config/redirects.yaml")?;
    let mut reader = BufReader::new(file);
    let redirect_array: Vec<RedirectCfg> = serde_yaml::from_reader(reader)?;

    let mut redirects = HashMap::new();
    for r in redirect_array {
        redirects.insert(format!("/{}", r.slug), r.to_url);
    }

    file = File::open("./config/general.yaml")?;
    reader = BufReader::new(file);
    let general_cfg: GeneralCfg = serde_yaml::from_reader(reader)?;

    let cfg = Config {
        redirects: redirects,
        general: general_cfg,
    };

    Ok(cfg)
}

#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    pretty_env_logger::init();

    let config: Config = load_cfg().unwrap();

    let cfg = Arc::new(config);
    let service = make_service_fn(|_conn| {
        let cfg = cfg.clone();
        async move { Ok::<_, hyper::Error>(service_fn(move |req| handle_request(cfg.clone(), req))) }
    });

    let addr = ([0, 0, 0, 0], 3000).into();

    let server = Server::bind(&addr).serve(service);

    println!("Listening on http://{}", addr);

    server.await?;

    Ok(())
}
